/*
 * Copyright (C) 2009 Rob Cash
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 * DefaultStateMachine.java
 * 
 * Created on Jan 1, 2009 at 12:55:41 PM
 */

package org.robcash.commons.util;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import javax.swing.event.EventListenerList;
import org.robcash.commons.event.StateChangeEvent;
import org.robcash.commons.event.StateChangeListener;

/**
 * Basic state machine
 * @param <T> Type of value contained by machine state
 * @author Rob
 */
public class DefaultStateMachine<T> implements StateMachine<T> {

    /**
     * Initial state
     */
    private State<T> initialState;

    /**
     * Current state
     */
    private State<T> currentState;

    /**
     * State transitions
     */
    private Map<State<T>, Set<StateTransition<T>>> stateTransitions;

    /**
     * Event listeners
     */
    private EventListenerList eventListeners;

    /**
     * Create a new default state machine
     */
    public DefaultStateMachine() {
        stateTransitions = new HashMap<State<T>, Set<StateTransition<T>>>();
        eventListeners = new EventListenerList();
    }

    /**
     * Add a state to the state machine
     * @param state State to add
     * @return true if the state was added; false if it was not added due to
     * having previously been added
     */
    public boolean addState(State<T> state) {
        if ( stateTransitions.containsKey(state) ) {
            return false;
        }
        stateTransitions.put(state, new HashSet<StateTransition<T>>());
        return true;
    }

    /**
     * Add a state to the state machine
     * @param state State to add
     * @param initialState Indicates if this is the initial state
     * @return true if the state was added; false if it was not added due to
     * having previously been added
     */
    public boolean addState(State<T> state, boolean initialState) {
        boolean returnValue = addState(state);
        if ( returnValue && initialState ) {
            this.initialState = state;
            this.currentState = state;
        }
        return returnValue;
    }

    /**
     * Add a transition from one state to another
     * @param transition State to state transition
     * @throws IllegalArgumentException Thrown if the origin state identified in
     * the transition does not exist in the state machine
     * @throws NullPointerException Thrown if the origin state or destination
     * state is null
     */
    public void addTransition(StateTransition<T> transition) {
        State<T> origin = transition.getOrigin();
        State<T> destination = transition.getDestination();

        // Validate origin and destination states
        if ( origin == null ) {
            throw new NullPointerException("Origin state cannot be null");
        }
        if ( !stateTransitions.containsKey(origin) ) {
            throw new IllegalArgumentException("Origin state does not belong to state machine");
        }
        if ( destination == null ) {
            throw new NullPointerException("Destination state cannot be null");
        }
        if ( !stateTransitions.containsKey(destination) ) {
            throw new IllegalArgumentException("Destination state does not belong to state machine");
        }

        // Add transition
        stateTransitions.get(origin).add(transition);
        if ( transition.isTwoWay() ) {
            stateTransitions.get(destination).add(transition);
        }
    }

    /**
     * Remove a state
     * @param state State to remove
     * @exception IllegalStateException Thrown if the state being removed is the current state
     */
    public void removeState(State<T> state) throws IllegalStateException {
        if ( currentState == state ) {
            throw new IllegalStateException("Cannot remove the current state");
        }

        // Look at each transition for this state to see if the other side of a
        // two-way transition needs to be deleted as well.
        for( StateTransition<T> aTransition : stateTransitions.get(state) ) {
            if ( aTransition.isTwoWay() ) {
                // This is a two-way transition: remove the reverse transition
                State<T> destination = aTransition.getDestination();
                if ( destination != state ) {
                    // Destination is different
                    // Remove this transition from the destination
                    stateTransitions.get(destination).remove(aTransition);
                }
            }
        }
        stateTransitions.remove(state);
    }

    /**
     * Remove a transition from one state to another
     * @param transition State to state transition
     */
    public void removeTransition(StateTransition<T> transition) {
        State<T> origin = transition.getOrigin();
        State<T> destination = transition.getDestination();

        // Before removing from the origin, make sure that this is not a two-way
        // transition. If it is, remove it from the desination as well.
        if ( transition.isTwoWay() && destination != origin ) {
            stateTransitions.get(destination).remove(transition);
        }
        stateTransitions.get(origin).remove(transition);
    }

    /**
     * Get the current state
     * @return current state
     */
    public State<T> getCurrentState() {
        return currentState;
    }

    /**
     * Get the transitions available for the specified state
     * @param state State whose transitions should be returned
     * @return Transitions for the specified state
     */
    public Set<StateTransition<T>> getTransitionsForState(State<T> state) {
        return stateTransitions.get(state);
    }

    /**
     * Indicates if the current state has a transition of the specified name
     * @param transitionName Name of transition to check
     * @return <tt>true</tt> if the current state has a transition named
     * <tt>name</tt>
     */
    public boolean canTransitionTo(String transitionName) {
        boolean transitionFound = false;
        for( StateTransition<T> transition : stateTransitions.get(currentState) ) {
            if ( transition.getName().equals(transitionName) ) {
                transitionFound = true;
                break;
            }
        }

        return transitionFound;
    }

    /**
     * Transition to a new state given the name of a transition
     * @param transition Transition to follow
     * @return New state
     * @throws IllegalStateException Thrown if the specified transition is not
     * valid for the current state
     */
    public State<T> transitionToNewState(StateTransition<T> transition) throws IllegalStateException {
        State<T> newState = null;
        if ( stateTransitions.get(currentState).contains(transition) ) {
            // Valid state change
            newState = transition.getDestination();
            StateChangeEvent event = new StateChangeEvent(this, currentState,
                    newState);
            fireStateChangeListenerStateChanging(event);
            currentState = newState;
            fireStateChangeListenerStateChanged(event);
        }
        else {
            // Invalid state change
            throw new IllegalStateException("Cannot transition from current " +
                    "state to state identified by transition " + transition);
        }

        return newState;
    }

    /**
     * Transition to a new state given the name of a transition
     * @param transitionName Name of transition to follow
     * @return New state
     * @throws NullPointerException Thrown if the transitionName is null
     * @throws IllegalStateException Thrown if the specified transition name is
     * not valid for the current state
     */
    public State<T> transitionToNewState(String transitionName) throws IllegalStateException {
        if ( transitionName == null ) {
            throw new NullPointerException("Transition name cannot be null");
        }
        if ( currentState == null ) {
            throw new IllegalStateException("No initial state");
        }
        State newState = null;
        for(StateTransition<T> aTransition : stateTransitions.get(currentState)) {
            if ( aTransition.getName().equals(transitionName) ) {
                newState = transitionToNewState(aTransition);
                break;
            }
        }

        if ( newState == null ) {
            throw new IllegalStateException("No transition for current state " +
                    "named '" + transitionName + "'");
        }

        return newState;
    }

    /**
     * Indicates that the current state is terminal. A state is terminal if it
     * has no transitions.
     * @return <tt>true</tt> if the current state has no transitions
     */
    public boolean isFinished() {
        return currentState != null &&
                stateTransitions.get(currentState).size() == 0;
    }

    /**
     * Reset the state machine to the initial state
     */
    public void reset() {
        currentState = initialState;
    }

    /**
     * Returns an iterator over the states in this state machine
     * @return an <tt>Iterator</tt> over the states in this state machine
     */
    public Iterator<State<T>> iterator() {
        return stateTransitions.keySet().iterator();
    }
    
    /**
     * Add state change listener
     * @param listener State change listener
     */
    public void addStateChangeListener(StateChangeListener listener) {
        eventListeners.add(StateChangeListener.class, listener);
    }

    /**
     * Add state change listener
     * @param listener State change listener
     */
    public void removeStateChangeListener(StateChangeListener listener) {
        eventListeners.remove(StateChangeListener.class, listener);
    }

    /**
     * Fire an event to state change listeners
     * @param event Event to fire
     */
    protected void fireStateChangeListenerStateChanging(StateChangeEvent event) {
        StateChangeListener[] listeners = eventListeners.getListeners(
                StateChangeListener.class);
        for( int index = 0; index < listeners.length; index++ ) {
            listeners[index].stateChanging(event);
        }
    }

    /**
     * Fire an event to state change listeners
     * @param event Event to fire
     */
    protected void fireStateChangeListenerStateChanged(StateChangeEvent event) {
        StateChangeListener[] listeners = eventListeners.getListeners(
                StateChangeListener.class);
        for( int index = 0; index < listeners.length; index++ ) {
            listeners[index].stateChanged(event);
        }
    }

}
